{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "from mabwiser.mab import LearningPolicy\n",
    "\n",
    "from alns import ALNS\n",
    "from alns.accept import *\n",
    "from alns.select import *\n",
    "from alns.stop import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "SEED = 42"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "np.random.seed(SEED)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# ALNS features\n",
    "\n",
    "The `alns` package offers a number of different operator selection schemes, and acceptance and stopping criteria. In this notebook, we show some these in action solving a toy knapsack problem. Along the way we explain how they work, and show how you can use them in your ALNS heuristic.\n",
    "\n",
    "In our toy [0/1-knapsack problem](https://en.wikipedia.org/wiki/Knapsack_problem), there are $n = 100$ items $i$ with profit $p_i > 0$ and weight $w_i > 0$. The goal is to find a subset of the items that maximizes the profit, while keeping the total weight below a given limit $W$. The problem then reads follows:\n",
    "\n",
    "$$ \\max_{x_i \\in \\{0, 1\\}} \\; \\sum_{i=1}^n p_i x_i $$\n",
    "subject to\n",
    "$$ \\sum_{i=1}^n w_i x_i \\le W $$\n",
    "\n",
    "First we quickly set up everything required for solving the problem with ALNS. In particular, we define a solution state, and a few destroy and repair operators. Our goal is not to solve this problem very well, so we set up only the bare minimum needed to get the ALNS algorithm going."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "n = 100\n",
    "p = np.random.randint(1, 100, size=n)\n",
    "w = np.random.randint(10, 50, size=n)\n",
    "W = 1_000\n",
    "\n",
    "# Percentage of items to remove in each iteration\n",
    "destroy_rate = 0.25"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "class KnapsackState:\n",
    "    \"\"\"\n",
    "    Solution class for the 0/1 knapsack problem. It stores the current\n",
    "    solution as a vector of binary variables, one for each item.\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, x: np.ndarray):\n",
    "        self.x = x\n",
    "\n",
    "    def objective(self) -> int:\n",
    "        # Negative p since ALNS expects a minimisation problem.\n",
    "        return -p @ self.x\n",
    "\n",
    "    def weight(self) -> int:\n",
    "        return w @ self.x"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Destroy operators\n",
    "\n",
    "We implement two operators:\n",
    "\n",
    "- A simple random destroy operator, which removes items from the knapsack at random.\n",
    "- A destroy operator that removes items based on their relative merits, that is, for an item $i$ currently in the knapsack, it removes those whose $p_i / w_i$ values are smallest."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def to_destroy(state: KnapsackState) -> int:\n",
    "    return int(destroy_rate * state.x.sum())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def random_remove(state: KnapsackState, rng):\n",
    "    probs = state.x / state.x.sum()\n",
    "    to_remove = rng.choice(np.arange(n), size=to_destroy(state), p=probs)\n",
    "\n",
    "    assignments = state.x.copy()\n",
    "    assignments[to_remove] = 0\n",
    "\n",
    "    return KnapsackState(x=assignments)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def worst_remove(state: KnapsackState, rng):\n",
    "    merit = state.x * p / w\n",
    "    by_merit = np.argsort(-merit)\n",
    "    by_merit = by_merit[by_merit > 0]\n",
    "    to_remove = by_merit[: to_destroy(state)]\n",
    "\n",
    "    assignments = state.x.copy()\n",
    "    assignments[to_remove] = 0\n",
    "\n",
    "    return KnapsackState(x=assignments)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Repair operators\n",
    "\n",
    "We implement only the random repair operator. The focus of this notebook is not on solving the knapsack problem very well, but rather to showcase the different operator schemes and acceptance criteria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def random_repair(state: KnapsackState, rng):\n",
    "    unselected = np.argwhere(state.x == 0)\n",
    "    rng.shuffle(unselected)\n",
    "\n",
    "    while True:\n",
    "        can_insert = w[unselected] <= W - state.weight()\n",
    "        unselected = unselected[can_insert]\n",
    "\n",
    "        if len(unselected) != 0:\n",
    "            insert, unselected = unselected[0], unselected[1:]\n",
    "            state.x[insert] = 1\n",
    "        else:\n",
    "            return state"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## ALNS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def make_alns() -> ALNS:\n",
    "    rng = np.random.default_rng(SEED)\n",
    "    alns = ALNS(rng)\n",
    "\n",
    "    alns.add_destroy_operator(random_remove)\n",
    "    alns.add_destroy_operator(worst_remove)\n",
    "\n",
    "    alns.add_repair_operator(random_repair)\n",
    "\n",
    "    return alns"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Terrible - but simple - first solution, where only the first item is\n",
    "# selected.\n",
    "init_sol = KnapsackState(np.zeros(n))\n",
    "init_sol.x[0] = 1"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Operator selection schemes\n",
    "\n",
    "We now have everything set-up for solving the problem. We will now look at several of the operator selection schemes the `alns` package offers. The list is not exhaustive: for a complete overview, head over to `alns.select` in the documentation.\n",
    "\n",
    "Here, we use the `HillClimbing` acceptance criterion, which only accepts better solutions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "accept = HillClimbing()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Roulette wheel\n",
    "\n",
    "The `RouletteWheel` scheme updates operator weights as a convex combination of the current weight, and the new score.\n",
    "\n",
    "When the algorithm starts, all operators $i$ are assigned weight $\\omega_i = 1$. In each iteration, a destroy and repair operator $d$ and $r$ are selected by the ALNS algorithm, based on the current weights $\\omega_i$. These operators are applied to the current solution, resulting in a new candidate solution. This candidate is evaluated by the ALNS algorithm, which leads to one of four outcomes:\n",
    "\n",
    "1. The candidate solution is a new global best.\n",
    "2. The candidate solution is better than the current solution, but not a global best.\n",
    "3. The candidate solution is accepted.\n",
    "4. The candidate solution is rejected.\n",
    "\n",
    "Each of these four outcomes is assigned a score $s_j~$ (with $j = 1,...,4$). After observing outcome $j$, the weights of the destroy and repair operator $d$ and $r$ that were applied are updated as follows:\n",
    "$$ \\omega_d = \\theta \\omega_d + (1 - \\theta) s_j, $$\n",
    "$$ \\omega_r = \\theta \\omega_r + (1 - \\theta) s_j, $$\n",
    "where $0 \\le \\theta \\le 1$ (known as the _operator decay rate_) is a parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "select = RouletteWheel(\n",
    "    scores=[5, 2, 1, 0.5], decay=0.8, num_destroy=2, num_repair=1\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Segmented roulette wheel\n",
    "\n",
    "The `RouletteWheel` scheme continuously updates the weights of the destroy and repair operators. As a consequence, it might overlook that different operators are more effective in the neighbourhood of different solutions.\n",
    "\n",
    "The `SegmentedRouletteWheel` scheme attempts to fix this, by fixing the operator weights $\\omega_i$ for a number of iterations (the _segment length_). Initially, all weights are set to one, as in `RouletteWheel`. A separate score is tracked for each operator $d$ and $r$, to which the observed scores $s_j$ are added in each iteration where $d$ and $r$ are applied. After the segment concludes, these summed scores are added to the weights $\\omega_i$ as a convex combination using a parameter $\\theta$ (the _segment decay rate_) as in `RouletteWheel`. The separate score list is then reset to zero, and a new segment begins."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "select = SegmentedRouletteWheel(\n",
    "    scores=[5, 2, 1, 0.5],\n",
    "    decay=0.8,\n",
    "    seg_length=500,\n",
    "    num_destroy=2,\n",
    "    num_repair=1,\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### $\\alpha$-UCB\n",
    "\n",
    "The $\\alpha$-UCB scheme is an upper confidence bound bandit algorithm that learns good (destroy, repair) operator pairs, and plays those more often during the search. The $\\alpha$ parameter controls the exploration performed by the learning algorithm: values of $\\alpha$ near one result in much exploration, whereas values of $\\alpha$ nearer to zero result in more exploitation of good operator pairs. Typically, $\\alpha \\le 0.1$ is a good choice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "select = AlphaUCB(\n",
    "    scores=[5, 2, 1, 0.5], alpha=0.05, num_destroy=2, num_repair=1\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### More advanced bandit algorithms\n",
    "\n",
    "Operator selection can be seen as a multi-armed-bandit problem.\n",
    "Each operator pair is a bandit arm, and the reward for each arm corresponds to the evaluation outcome depending on the score array.\n",
    "Accordingly, any multi-armed bandit algorithm can be used as an operator selection scheme.\n",
    "ALNS integrates with [MABWiser](https://github.com/fidelity/mabwiser/) to provide access to more bandit algorithms. \n",
    "You may install MABWiser as an extra dependency using `pip install alns[mabwiser]`.\n",
    "\n",
    "Here, we use a simple epsilon-greedy algorithm from MABWiser.\n",
    "The algorithm picks a random operator pair with probability $\\epsilon=0.15$ and otherwise chooses the operator pair with the largest mean so far."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "select = MABSelector(\n",
    "    scores=[5, 2, 1, 0.5],\n",
    "    num_destroy=2,\n",
    "    num_repair=1,\n",
    "    learning_policy=LearningPolicy.EpsilonGreedy(epsilon=0.15),\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Contextual bandit algorithms\n",
    "\n",
    "Some MABWiser bandit algorithms require a context vector when making an operator selection choice.\n",
    "To utilize these algorithms, your `State` class must additionally conform to the `ContextualState` protocol found in `ALNS.State`.\n",
    "In practice, this simply means adding an additional method that takes no arguments and returns context data about your state.\n",
    "Here we monkey-patch our existing `KnapsackState` to conform to the `ContextualState` protocol."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_knapsack_context(self: KnapsackState):\n",
    "    num_items = np.count_nonzero(self.x)\n",
    "    avg_weight = self.weight() / num_items\n",
    "    return np.array([self.weight(), num_items, avg_weight])\n",
    "\n",
    "\n",
    "KnapsackState.get_context = get_knapsack_context"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now use a contextual bandit algorithm such as `LinGreedy`. See the [MABWiser documentation](https://fidelity.github.io/mabwiser/api.html#mabwiser.mab.LearningPolicy) for more details on how this and other contextual bandit algorithms work."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "select = MABSelector(\n",
    "    scores=[5, 2, 1, 0.5],\n",
    "    num_destroy=2,\n",
    "    num_repair=1,\n",
    "    learning_policy=LearningPolicy.LinGreedy(epsilon=0.15),\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Acceptance criteria\n",
    "\n",
    "We have just looked at the different weight schemes, using a fixed acceptance criterion. Now we flip this around: we fix an operator selection scheme, and look at several acceptance criteria the `alns` package offers. Note that this list is not exhaustive: for a look at all available acceptance criteria, head over to `alns.accept`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "select = SegmentedRouletteWheel(\n",
    "    scores=[5, 2, 1, 0.5],\n",
    "    decay=0.8,\n",
    "    seg_length=500,\n",
    "    num_destroy=2,\n",
    "    num_repair=1,\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Hill climbing\n",
    "\n",
    "This acceptance criterion only accepts better solutions. It was used in the examples explaining the operator selection schemes, so we will not repeat it here. You might also be interested in the other example notebooks for the cutting stock and travelling salesman problems, which also rely on this acceptance criterion."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Record-to-record travel\n",
    "\n",
    "This criterion accepts solutions when the improvement meets some updating threshold.\n",
    "\n",
    "In particular, consider the current best solution $s^*$ with objective $f(s^*)$. A new candidate solution $s^c$ is accepted if the improvement $f(s^c) - f(s^*)$ is smaller than some updating threshold $t$. This threshold is initialised at some starting value, and then updated using a step value $u$. There are two ways in which this update can take place:\n",
    "\n",
    "- _linear_: the threshold is updated linearly, as $t = t - u$.\n",
    "- _exponential_: the threshold is updated exponentially, as $t = t \\times u$.\n",
    "\n",
    "Finally, the threshold $t$ cannot become smaller than a minimum value, the end threshold."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "accept = RecordToRecordTravel(\n",
    "    start_threshold=255, end_threshold=5, step=250 / 10_000, method=\"linear\"\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Simulated annealing\n",
    "\n",
    "This criterion accepts solutions when the scaled probability is bigger than some random number, using an updating temperature that drives the probability down. It is very similar to the `RecordToRecordTravel` criterion, but uses a different acceptance scheme.\n",
    "\n",
    "In particular, a temperature is used, rather than a threshold, and the candidate $s^c$ is compared against the current solution $s$, rather than the current best solution $s^*$. The acceptance probability is calculated as\n",
    "$$ \\exp \\left\\{ \\frac{f(s) - f(s^c)}{t} \\right\\}, $$\n",
    "where $t$ is the current temperature."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "accept = SimulatedAnnealing(\n",
    "    start_temperature=1_000,\n",
    "    end_temperature=1,\n",
    "    step=1 - 1e-3,\n",
    "    method=\"exponential\",\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxIterations(10_000))\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "Rather than a fixed number of iterations, we can also fix the runtime, and allow as many iterations as fit in that timeframe."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "accept = SimulatedAnnealing(\n",
    "    start_temperature=1_000,\n",
    "    end_temperature=1,\n",
    "    step=1 - 1e-3,\n",
    "    method=\"exponential\",\n",
    ")\n",
    "\n",
    "alns = make_alns()\n",
    "res = alns.iterate(init_sol, select, accept, MaxRuntime(60))  # one minute\n",
    "\n",
    "print(f\"Found solution with objective {-res.best_state.objective()}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "_, ax = plt.subplots(figsize=(12, 6))\n",
    "res.plot_objectives(ax=ax, lw=2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Other stopping criteria are available in `alns.stop`."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Conclusions\n",
    "\n",
    "This notebook has shown the various operator selection schemes, acceptance and stopping criteria that can be used with the `alns` package.\n",
    "The `alns` package is designed to be flexible, and it is easy to add new schemes and criteria yourself, by subclassing `alns.select.OperatorSelectionScheme`, `alns.accept.AcceptanceCriterion`, or `alns.stop.StoppingCriterion`.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.19"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
